﻿using System;
using System.Data;
using System.Data.SqlClient;
using System.Text;
using System.Reflection;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Configuration;

using Open.Data.Persistence;


namespace Open.Data.Persistence.SqlServer
{
    //Implements a QueryDataAdapter for the SqlServer provider.
    public class SqlQueryDataAdapter: QueryDataAdapter
    {        
        //Properties
        private SqlDataReader _reader;

        //Working Variables
        private SqlFormatter _formatter;
        private bool _initialised;
        
        public SqlQueryDataAdapter(SqlCommand command): base(command)
        {
        }

        public override void AddDataBinder<T>(DataBinder<T> binder)
        {
            //Set the binder's data adapter reference to this adapter
            binder.DataAdapter = this;

            //Create a binder info object for each binder in the binders collection
            IQuery query = binder.Query;
            BinderInfo info = new BinderInfo();
            CacheItem item = SqlProvider.Instance.ReflectionCache.Lookup(query.ActivateType);

            //Set values
            info.Query = query;
            info.Binder = binder;
            info.Label = Common.GetQueryLabel(query);
            info.TableName = Common.GetTypeName(query.Type, _formatter.Prefix);
            info.Indexes = item.GetKeyProperties();
            info.Keys = new Dictionary<string, byte>();
            info.Objects = new List<IPersistable>();

            DataBinders.Add(info);
        }

        public override bool Read()
        {
            //Validate
            if (DataBinders.Count == 0) throw new QueryDataAdapterException("A query record cannot be read as no DataBinders have been added to this QueryDataAdapter.");

            //Set up the reader if this is the first read
            if (_initialised == false) Initialise();

            //Read the next record
            bool result = _reader.Read();

            if (result)
            {
                //Resolve a key for each data binder
                foreach (BinderInfo info in DataBinders)
                {
                    //Get the key values as a string for the current row
                    string keyValue = null;
                    int index = 0;

                    //Create a string concatenation of the key values
                    foreach (int ordinal in info.Ordinals)
                    {
                        //Check for null keys in outer joins
                        if (_reader.IsDBNull(ordinal))
                        {
                            keyValue = null;
                            break;
                        }

                        //Get value of each key and append
                        if (info.Indexes[index].PropertyType.IsEnum)
                        {
                            keyValue += _reader.GetInt32(ordinal).ToString();
                        }
                        else
                        {
                            keyValue += _reader.GetValue(ordinal);
                        }
                        index++;
                    }

                    //Determine whether the object values have changed on this read
                    info.Binder.HasChanged = (info.PreviousKey != keyValue);

                    //Only create an object if the query is being filled
                    if (info.Binder.Query.Fill)
                    {
                        TypeInfo typeInfo = null;
                        IPersistable item = null;

                        //Set binder item to null
                        if (keyValue == null)
                        {
                            info.Binder.ClearItem();
                        }
                        else
                        {
                            //Lookup the key for this type, returning the item if found
                            if (Types != null)
                            {
                                typeInfo = Types[info.Query.ActivateType];
                                typeInfo.Objects.TryGetValue(keyValue, out item);
                            }

                            //If this object for this type and key has not already been created then bind
                            if (item == null)
                            {
                                //Set the formatter to the correct label for the bind
                                _formatter.Label = info.Label;
                                info.Binder.Formatter = _formatter;

                                //Bind over the existing object
                                info.Binder.Bind();
                                info.Binder.HasChanged = true;

                                //Add an entry for the object to the type objects
                                if (Types != null)
                                {
                                    item = info.Binder.GetItem();
                                    typeInfo.Objects.Add(keyValue, item);

                                    //Add an entry to the binder keys and objects
                                    info.Keys.Add(keyValue, 0);
                                    info.Objects.Add(item);
                                }
                            }
                            else //Determine if the binder has already parsed this object
                            {
                                if (!info.Keys.ContainsKey(keyValue))
                                {
                                    info.Binder.HasChanged = true;
                                    info.Keys.Add(keyValue, 0);
                                    info.Objects.Add(item);
                                }
                            }
                        }
                    }
                    info.PreviousKey = keyValue;
                }
            }
            else
            {
                _reader.Close();
            }

            return result;
        }

        public override List<T> CreateList<T>(IDataBinder binder)
        {
            //Create the internal lists of objects
            if (Types == null) CreateLists();

            List<T> result = new List<T>();

            //Clear each item
            foreach (BinderInfo info in DataBinders)
            {
                if (info.Binder == binder)
                {
                    foreach (T item in info.Objects)
                    {
                        result.Add(item);
                    }
                }
            }

            return result;
        }

        public override void Reset()
        {
            if (_reader != null)
            {
                _reader.Close();
                _reader = null;
            }

            _initialised = false;

            base.Reset();

            //Create our own internal formatter so that it is not changed between reads
            _formatter = Command.Context.CreateFormatter() as SqlFormatter;
            _formatter.Command = Command;

            //Set up the formatter
            _formatter.WithConcurrency = false;
            _formatter.WithInheritance = true;
            _formatter.WithKeys = true;
            _formatter.WithIdentity = false;
            _formatter.Prefix = Command.Context.Prefix;
        }

        public override void Dispose()
        {
            if (_reader != null) _reader.Close();
        }

        public override void Initialise()
        {
            IQuery query = DataBinders[0].Query;
            IQuery root = Common.GetRootQuery(query);

            //Get the reader by executing the query
            _reader = Command.ExecuteReader(root) as SqlDataReader;

            //Set formatter
            _formatter.Ordinals = new Ordinals(root);
            _formatter.Reader = _reader;
            _formatter.Adapter = this;

            //Calculate primary key ordinals
            foreach (BinderInfo info in DataBinders)
            {
                info.Ordinals = new List<int>();

                foreach (PropertyInfo prop in info.Indexes)
                {
                    string name = Formatter.GetPropertyName(prop, ".");
                    
                    int value; //The type position in the ordinals collection
                    if (!_formatter.Ordinals.TryGetValue(info.Label, out value)) throw new PersistenceException(string.Format("Type information for {0} was not found in the query.",info.Label));
                    int ordinal = _formatter.GetOrdinal(info.TableName, name, value);
                    info.Ordinals.Add(ordinal);
                }
            }

            _initialised = true;
        }

        //Loops through the reader and creates a graph of objects
        private void CreateLists()
        {
            Types = new Dictionary<Type, TypeInfo>();

            //Set up type info so that we can store all object references by type
            foreach (BinderInfo info in DataBinders)
            {
                //Add a new type info object for each query activate type
                if (!Types.ContainsKey(info.Query.ActivateType))
                {
                    TypeInfo typeInfo = new TypeInfo();

                    typeInfo.Type = info.Query.ActivateType;
                    typeInfo.Objects = new Dictionary<object, IPersistable>();

                    Types.Add(typeInfo.Type, typeInfo);
                }
            }

            while (Read())
            {
                //Clear each item reference so that a new object is created after each read
                foreach (BinderInfo info in DataBinders)
                {
                    info.Binder.ClearItem();
                }
            }
        }
    }
}
